package com.spring.boot;

import com.spring.beans.BeanUtils;
import com.spring.context.ApplicationContext;
import com.spring.context.ApplicationContextInitializer;
import com.spring.context.ApplicationListener;
import com.spring.context.ConfigurableApplicationContext;
import com.spring.core.annotation.AnnotationAwareOrderComparator;
import com.spring.core.env.ConfigurableEnvironment;
import com.spring.core.io.DefaultResourceLoader;
import com.spring.core.io.ResourceLoader;
import com.spring.core.io.support.SpringFactoriesLoader;
import com.spring.core.metrics.ApplicationStartup;
import com.spring.logging.Log;
import com.spring.logging.LogFactory;
import com.spring.util.ClassUtils;

import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * @author liyuncong
 * @version 1.0
 * @file com.spring.boot.SpringApplication
 * @brief com.spring.boot.SpringApplication
 * @details spring启动
 * @date 2022-12-08
 *
 * Edit History
 * ----------------------------------------------------------------------------
 * DATE                        NAME               DESCRIPTION
 * 2022-12-08                  liyuncong          Created
 */
public class SpringApplication {

    private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";

    private static final Log logger = LogFactory.getLog(SpringApplication.class);
    static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();

    private Set<Class<?>> primarySources;
    private Class<?> mainApplicationClass;

    private boolean addConversionService = true;

    private ResourceLoader resourceLoader;
    private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;

    private ConfigurableEnvironment environment;
    private WebApplicationType webApplicationType;

    private boolean headless = true;

    private Banner.Mode bannerModel = Banner.Mode.CONSOLE;
    private Banner banner;

    private boolean logStartupInfo = true;
    private boolean registerShutdownHook = true;

    private List<ApplicationContextInitializer<?>> initializers;

    private List<ApplicationListener<?>> listeners;

    private List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;

    private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;

    public SpringApplication(Class<?>... primarySources) {
        this(null, primarySources);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
        setInitializers(
            (Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

    private Class<?> deduceMainApplicationClass() {
        try {
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        } catch (ClassNotFoundException exception) {
            // 丢弃异常并继续
        }
        return null;
    }

    public ConfigurableApplicationContext run(String... args) {
        final long startTime = System.nanoTime();
        DefaultBootstrapContext bootstrapContext = createBootstrapContext();
        ConfigurableApplicationContext context = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment =
                prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            context.setApplicationStartup(applicationStartup);
            prepareContext(bootstrapContext, context, environment,
                listeners, applicationArguments, printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                // new StartupInfoLogger
            }
            listeners.started(context, timeTakenToStartup);
            callRunners(context, applicationArguments);
        } catch (Throwable throwable) {
            handlerRunFailure(context, throwable, listeners);
            throw new IllegalStateException(throwable);
        }
        try {
            final Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
            listeners.ready(context, timeTakenToReady);
        } catch (Throwable throwable) {
            handlerRunFailure(context, throwable, null);
            throw new IllegalStateException(throwable);
        }
        return context;
    }


    protected void refresh(ConfigurableApplicationContext applicationContext) {
        applicationContext.refresh();
    }

    protected void afterRefresh(
        ConfigurableApplicationContext context, ApplicationArguments applicationArguments
    ) {
        // TODO
    }

    private void callRunners(ApplicationContext context, ApplicationArguments arguments) {
        // TODO
    }

    private void handlerRunFailure(
        ConfigurableApplicationContext context,
        Throwable throwable,
        SpringApplicationRunListeners listeners
    ) {
        // TODO
    }

    private void refreshContext(ConfigurableApplicationContext context) {
        // TODO
        if (this.registerShutdownHook) {
            shutdownHook.registerApplicationContext(context);
        }
        refresh(context);
    }


    private void prepareContext(
        DefaultBootstrapContext bootstrapContext,
        ConfigurableApplicationContext context,
        ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments,
        Banner printedBanner
    ) {
        // TODO
    }

    protected ConfigurableApplicationContext createApplicationContext() {
        return this.applicationContextFactory.create(this.webApplicationType);
    }

    private Banner printBanner(ConfigurableEnvironment environment) {
        if (this.bannerModel == Banner.Mode.OFF) {
            return null;
        }
        ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
            : new DefaultResourceLoader(null);
        SpringApplicationBannerPrinter bannerPrinter =
            new SpringApplicationBannerPrinter(resourceLoader, banner);
        if (this.bannerModel == Banner.Mode.LOG) {
//            return bannerPrinter.print(environment, )
            return null;
        }
        return banner;
    }

    private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
        // TODO
    }

    private void configureHeadlessProperty() {
        System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
            System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
    }

    private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        DefaultBootstrapContext bootstrapContext,
        ApplicationArguments applicationArguments
    ) {
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());

        // TODO

        listeners.environmentPrepared(bootstrapContext, environment);
        return environment;
    }

    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class<?>[] {SpringApplication.class, String[].class};
        return new SpringApplicationRunListeners(
            logger,
            getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
            this.applicationStartup
        );
    }

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            // TODO
        }
    }

    private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        return switch (this.webApplicationType) {
            case SERVLET -> new ApplicationServletEnvironment();
            case REACTIVE -> new ApplicationReactiveWebEnvironment();
            default -> new ApplicationEnvironment();
        };
    }

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
        return getSpringFactoriesInstances(type, new Class<?>[] {});
    }

    private <T> Collection<T> getSpringFactoriesInstances(
        Class<T> type, Class<?>[] parameterTypes, Object... args
    ) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names =
            new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances =
            createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> createSpringFactoriesInstances(
        Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set<String> names
    ) {
        List<T> instances = new ArrayList<>(names.size());
        for (String name : names) {
            try {
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                T instance = (T) BeanUtils.instantiateClass(constructor, args);
                instances.add(instance);
            } catch (Throwable ex) {
                throw new IllegalStateException("Cannot instantiate " + type + " : " + name, ex);
            }
        }
        return instances;
    }

    public ClassLoader getClassLoader() {
        if (this.resourceLoader != null) {
            return this.resourceLoader.getClassLoader();
        }
        return ClassUtils.getDefaultClassLoader();
    }

    private DefaultBootstrapContext createBootstrapContext() {
        DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
        this.bootstrapRegistryInitializers.forEach(
            (initializer) -> initializer.initialize(bootstrapContext));
        return bootstrapContext;
    }

    public static Object run(Class<?> primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
    }

    private static Object run(Class<?>[] primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(new Class<?>[0], args);
    }

    public void setInitializers(
        Collection<? extends ApplicationContextInitializer<?>> initializers
    ) {
        this.initializers = new ArrayList<>(initializers);
    }

    public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
        this.listeners = new ArrayList<>(listeners);
    }


}
